package org.munoz.server;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.munoz.client.GoFishService;
import org.munoz.shared.GlickoRating;
import org.munoz.shared.LoginInfo;
import org.munoz.shared.Match;
import org.munoz.shared.Move;
import org.munoz.shared.PlayerOnline;
import org.munoz.shared.State;
import org.munoz.shared.StateChanger;

import com.google.appengine.api.channel.ChannelMessage;
import com.google.appengine.api.channel.ChannelService;
import com.google.appengine.api.channel.ChannelServiceFactory;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.google.gwt.user.server.rpc.XsrfProtectedServiceServlet;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.LoadResult;
import com.googlecode.objectify.ObjectifyService;
import com.googlecode.objectify.cmd.Query;

import static com.googlecode.objectify.ObjectifyService.ofy;


/**
 * Implements GoFishService 
 * @author jpm
 * based on NYU Games Spring 2013 code by Harsh Mehta
 */
public class GoFishServiceImpl extends RemoteServiceServlet implements GoFishService{ 
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	ChannelService channelService = ChannelServiceFactory.getChannelService();
	private String waitingPlayer="";
	private Map<String, String> hash = new HashMap<String, String>();
	String PLAYER1 = "0";
	String PLAYER2 = "1";
	String PLAYERSMATCHEDALREADY = "99";
	String initialState;
	UserService userService = UserServiceFactory.getUserService();
	
	static {
		ObjectifyService.register(PlayerOnline.class);
		ObjectifyService.register(Match.class);
	}
	
	/* (non-Javadoc)
	 * @see org.munoz.client.GoFishService#login(java.lang.String)
	 */
	@Override
	public LoginInfo login(String requestUri, LoginInfo loginInfo){
//		UserService userService = UserServiceFactory.getUserService();
//		User user = userService.getCurrentUser();
//		LoginInfo loginInfo = new LoginInfo();
		ChannelService channelService = ChannelServiceFactory.getChannelService();
		
//		if(user != null){
//			loginInfo.setLoggedIn(true);
//			loginInfo.setEmailAddress(user.getEmail());
//			loginInfo.setNickname(user.getNickname());
//			loginInfo.setLogoutUrl(userService.createLogoutURL(requestUri));
//			String token = channelService.createChannel(loginInfo.getEmailAddress());
//			loginInfo.setToken(token);
			// From connectPlayer
			String channelId = loginInfo.getUserFBId();
			Key<PlayerOnline> playerK = Key.create(PlayerOnline.class, loginInfo.getUserFBId()); //user.getEmail());
			PlayerOnline player = ofy().load().key(playerK).now();
			if(player == null){
				player = new PlayerOnline(loginInfo.getUserFBId(), loginInfo.getFBUserName());//user.getEmail(), user.getNickname());
				System.out.println(player.getUserFBId() + " was created in Login");
			}
			String token = channelService.createChannel(channelId);	
			loginInfo.setToken(token);
//			System.out.println("In Login: " + user.getEmail() + " token " + token);
			
			int lowR = (int)(player.getRating()-(2*player.getRatingDev()));
			int hiR = (int)(player.getRating()+(2*player.getRatingDev()));
			loginInfo.setGlickoRange("[" + lowR + ", " + hiR + "]");
			ofy().save().entity(player).now();

//		}
//		else{
//			loginInfo.setLoggedIn(false);
//			loginInfo.setLoginUrl(userService.createLoginURL(requestUri));
//		}
		return loginInfo;
	}
	
	/* (non-Javadoc)
	 * @see org.munoz.client.GoFishService#connectPlayer()
	 */
	public String connectPlayer(){
		if(!userService.isUserLoggedIn()){
			return null;
		}
		User user = userService.getCurrentUser();
		String channelId = user.getEmail();
		Key<PlayerOnline> playerK = Key.create(PlayerOnline.class, user.getEmail());
		PlayerOnline player = ofy().load().key(playerK).now();
		if(player == null){
//			player = new PlayerOnline(user.getEmail(), user.getNickname());
		}
		ofy().save().entity(player).now();
		String token = channelService.createChannel(channelId);		
		
		System.out.println(user.getEmail() + " connected in playerConnect() with token " + token);
		
		return token;
	}
	
	/* (non-Javadoc)
	 * @see org.munoz.client.GoFishService#loadMatches()
	 */
	@Override
	public String[] loadMatches(LoginInfo loginInfo) {
		System.out.println("In loadMatches()");
		//if(userService.isUserLoggedIn()){
			//User user = userService.getCurrentUser();
		if(loginInfo.isLoggedIn()){
			Key<PlayerOnline> playerKey = Key.create(PlayerOnline.class, loginInfo.getUserFBId());//user.getEmail());
			
			// JP HW#8
//			PlayerOnline player = ofy().load().key(playerKey).now();
//			List<Match> t = ofy().load().type(Match.class).filter("player", playerKey).list();
			
			PlayerOnline player = ofy().load().key(playerKey).now();
			String [] matches = new String[player.getMatches().size()];
			Set<Key<Match>> matchesList = player.getMatches();
			int index = 0;
			for(Key<Match> key : matchesList){
				Match match = ofy().load().key(key).now();
				if(match == null){
					continue;
				}
				Key<PlayerOnline> opponentKey = match.getOpponent(playerKey);
				PlayerOnline opponent = ofy().load().key(opponentKey).now();
//				String turn;
//				if((match.isPlayer1(playerKey) && match.isPlayer1Turn()) ||
//					(match.isPlayer2(playerKey) && !match.isPlayer1Turn())){
//					turn = player.getName();
//				}
//				else{
//					turn = opponent.getName();
//				}
//				String player1 = (match.isPlayer1(playerKey))? player.getUserEmail() : opponent.getUserEmail();
				matches[index] = match.getId() + "#" + opponent.getUserFBId() + "#" 
						+ "#" + opponent.getName() + "#" /*+ turn + "#" + player1 + "#"*/ + match.getState();
				ofy().save().entities(match, opponent).now();
				index++;
			}
			ofy().save().entities(player).now();
			// TODO: Check all matches to find player as opponent
			return matches;
		}
		Window.alert("loadMatches() returning null");
		return null;
	}
	
	/* (non-Javadoc)
	 * @see org.munoz.client.GoFishService#automatch()
	 */
	@Override
	public void automatch(LoginInfo loginI) {

//		if(!userService.isUserLoggedIn()){
		if(!loginI.isLoggedIn()){
			return; 
		}
//		User user = userService.getCurrentUser();
//		System.out.println(user.getEmail() + " in automatch");
		Key<PlayerOnline> playerKey = Key.create(PlayerOnline.class, loginI.getUserFBId());//user.getEmail());
		PlayerOnline player = ofy().load().key(playerKey).now();
		System.out.println(player.getUserFBId() + " player was loaded from database");
		// List of players waiting
		List<PlayerOnline> otherPlayers = ofy().load().type(PlayerOnline.class).filter("automatchPooled", true).list();
		if(otherPlayers.isEmpty()){
			player.setAutomatchEligible(true);
			player.addToken(loginI.getUserFBId());//user.getEmail());
			ofy().save().entity(player).now();
			System.out.println(player.getUserFBId() + " was put in the automatch pool and save it to databse. Pool was empty");
			return;
		}
		else{
			System.out.println(/*user.getEmail()*/ loginI.getUserFBId() + " automatch pool is not empty");
			System.out.println("Size of automatch pool " + otherPlayers.size());
			boolean matched = false;
			PlayerOnline randomPlayer = null;
			for(PlayerOnline p : otherPlayers){
				System.out.println("Checking pool to match");
				System.out.println(p.getUserFBId() + " In match autopool");
				if(p.getUserFBId().compareTo(player.getUserFBId()) !=0){
					randomPlayer = p;
					System.out.println("Random Player " + randomPlayer.getUserFBId());
					matched = true;
					break;
				}
			}
			if(!matched){
				System.out.println("In automatch " + player.getUserFBId() + " not matched and saved");
				ofy().save().entity(player).now();
				return;
			}			
//			otherPlayers.remove(randomPlayer);
//			if(otherPlayers.get(0).equals(player)){ // TODO: Check this
//				ofy().save().entity(player).now();
//				return;
//			}
//			PlayerOnline randomPlayer = otherPlayers.remove(0);
			randomPlayer.setAutomatchEligible(false);
			System.out.println(randomPlayer.getUserFBId() + " was removed from pool");
			Key<PlayerOnline> randomPKey = Key.create(PlayerOnline.class, randomPlayer.getUserFBId());
			// 
			Match match = new Match(playerKey, player.getUserFBId(), randomPKey, randomPlayer.getUserFBId(), State.initialState()); 
			Key<Match> matchKey = ofy().save().entity(match).now();
			System.out.println("match saved: " + match.getPlayer() + " Opponent " + match.getOpponent(playerKey));
			player.addMatch(matchKey);
			randomPlayer.addMatch(matchKey);
//			String initialState = State.initialState();
			String message1 = "newgame#" + match.getId() + "#1#" + player.getName() +"#" + match.getMatchStartDate() + "#" + match.getState();// initialState;
			String message2 = "newgame#" + match.getId() + "#0#" + randomPlayer.getName() + "#" + match.getMatchStartDate() + "#" + match.getState();// initialState;
			System.out.println(message1);
			System.out.println(message2);
			Set<String> tokens1 = randomPlayer.getTokens();
			Set<String> tokens2 = player.getTokens();
			ofy().save().entities(player, randomPlayer).now();			
			for(String connection : tokens1){
				channelService.sendMessage(new ChannelMessage(connection, message1));
				System.out.println(connection + " in randomPlayer's tokens " + randomPlayer.getUserFBId());
			}
			for(String connection : tokens2){
				channelService.sendMessage(new ChannelMessage(connection, message2));
				System.out.println(connection + " in player's tokens" + player.getUserFBId());
			}
			System.out.println("Connections should have been printed");
			System.out.println(player.getName() + " playing against " + randomPlayer.getName());
		}
	}
	
	/* (non-Javadoc)
	 * @see org.munoz.client.GoFishService#newGameFromFBId(java.lang.String)
	 */
	@Override
	public Boolean newGameFromFBId(String oppFBId, LoginInfo loginI){
		if(!loginI.isLoggedIn()){
			return new Boolean(false);
		}
		Key<PlayerOnline> playerKey = Key.create(PlayerOnline.class,  loginI.getUserFBId());//user.getEmail());
		PlayerOnline player = ofy().load().key(playerKey).now();
		Key<PlayerOnline> opponentKey = Key.create(PlayerOnline.class, oppFBId);
		PlayerOnline opponent = ofy().load().key(opponentKey).now();
		if(opponent == null){
			return new Boolean(false);
		}
		Match match = new Match(playerKey, player.getUserFBId(), opponentKey, opponent.getUserFBId(), State.initialState());
		Key<Match> matchKey = ofy().save().entity(match).now();
		player.addMatch(matchKey);
		opponent.addMatch(matchKey);
		// 
		String initialState = State.initialState();
		String message1 = "newgame#" + match.getId() + "#0#" + opponent.getName() + "#" + match.getMatchStartDate() + "#" + initialState; 
		String message2 = "newgame#" + match.getId() + "#1#" + player.getName() + "#" + match.getMatchStartDate() + "#" + initialState;
		Set<String> tokens1 = player.getTokens();
		Set<String> tokens2 =  opponent.getTokens();
		ofy().save().entities(player, opponent, match).now();
		for(String connection : tokens1){
			channelService.sendMessage(new ChannelMessage(connection, message1));
		}
		for(String connection : tokens2){
			channelService.sendMessage(new ChannelMessage(connection, message2));
		}
		return new Boolean(true);
	}
	
	// TODO: Delete old newEmailGame(String email)
	/* (non-Javadoc)
	 * @see org.munoz.client.GoFishService#newGameFromEmail(java.lang.String)
	 */
	@Override
	public Boolean newGameFromEmail(String email, LoginInfo loginI){
//		if(!userService.isUserLoggedIn()){
		if(!loginI.isLoggedIn()){
			return new Boolean(false);
		}
//		User user = userService.getCurrentUser();
		Key<PlayerOnline> playerKey = Key.create(PlayerOnline.class,  loginI.getUserFBId());//user.getEmail());
		PlayerOnline player = ofy().load().key(playerKey).now();
		Key<PlayerOnline> opponentKey = Key.create(PlayerOnline.class, email);
		PlayerOnline opponent = ofy().load().key(opponentKey).now();
		if(opponent == null){
			return new Boolean(false);
		}
		Match match = new Match(playerKey, player.getUserFBId(), opponentKey, opponent.getUserFBId(), State.initialState());
		Key<Match> matchKey = ofy().save().entity(match).now();
		player.addMatch(matchKey);
		opponent.addMatch(matchKey);
		// 
		String initialState = State.initialState();
		String message1 = "newgame#" + match.getId() + "#0#" + opponent.getName() + "#" + match.getMatchStartDate() + "#" + initialState; 
		String message2 = "newgame#" + match.getId() + "#1#" + player.getName() + "#" + match.getMatchStartDate() + "#" + initialState;
		Set<String> tokens1 = player.getTokens();
		Set<String> tokens2 =  opponent.getTokens();
		ofy().save().entities(player, opponent, match).now();
		for(String connection : tokens1){
			channelService.sendMessage(new ChannelMessage(connection, message1));
		}
		for(String connection : tokens2){
			channelService.sendMessage(new ChannelMessage(connection, message2));
		}
		return new Boolean(true);
	}
	
	/* (non-Javadoc)
	 * @see org.munoz.client.GoFishService#deleteMatch(java.lang.Long)
	 */
	@Override
	public void deleteMatch(Long matchId, LoginInfo loginI){
		if(matchId == null){
			return;
		}
//		if(userService.isUserLoggedIn()){
		if(!loginI.isLoggedIn()){
//			User user = userService.getCurrentUser();
			Key<PlayerOnline>  playerKey = Key.create(PlayerOnline.class, loginI.getUserFBId());//user.getEmail());
			PlayerOnline player = ofy().load().key(playerKey).now();
			Key<Match> matchKey = Key.create(Match.class, matchId);
			player.removeMatch(matchKey);
			ofy().save().entity(player).now();
			
			Match match = ofy().load().key(matchKey).now();
//			PlayerOnline opponent = ofy().load().key(match.getOpponent(playerKey)).now();
			// 
//			if(!opponent.containsMatchKey(matchKey)){
				ofy().delete().entity(match);
//			}
//			ofy().save().entities(match, opponent).now();
		}
	}
	
	/* (non-Javadoc)
	 * @see org.munoz.client.GoFishService#changeMatch(java.lang.Long)
	 */
	@Override
	public String changeMatch(Long matchId, LoginInfo loginI){
//		if(!userService.isUserLoggedIn()){
		if(!loginI.isLoggedIn()){
			return null;
		}
		if(matchId == null){
			return "no match";
		}
//		User user = userService.getCurrentUser();
		Key<PlayerOnline> playerKey = Key.create(PlayerOnline.class, loginI.getUserFBId());//user.getEmail());
		PlayerOnline player = ofy().load().key(playerKey).now();
		Key<Match> matchKey = Key.create(Match.class, matchId);
		Match match = ofy().load().key(matchKey).now();
		String stateStr = match.getState();
		Key<PlayerOnline> opponentKey = match.getOpponent(playerKey);
		PlayerOnline opponent = ofy().load().key(opponentKey).now();
		int turn;
//		String turn;
//		if((match.isPlayer1(playerKey) && match.isPlayer1Turn()) || 
//				(match.isPlayer2(playerKey) && !match.isPlayer1Turn())){
//			turn = user.getEmail();
//		}
//		else{
//			turn = opponent.getUserEmail();
//		}
		if(match.isPlayer1(playerKey)){
			turn = State.PLAYER1;
		}
		else{
			turn = State.PLAYER2;
		}
//		String message1 = "newgame#" + match.getId() + "#0#" + player.getName() +"#" + match.getState();// initialState;
		String msg = matchId+"#"+turn+"#"+opponent.getName()+"#"+match.getMatchStartDate()+"#"+stateStr;
		ofy().save().entities(match, player, opponent).now();
		return msg;
	}

	/* (non-Javadoc)
	 * @see org.munoz.client.GoFishService#makeMove(java.lang.Long, java.lang.String, java.lang.String)
	 */
	@Override
	public void makeMove(Long matchId, String move, String state, LoginInfo loginI){
//		System.out.println("In makeMove " + matchId);
//		if(!userService.isUserLoggedIn()){
		if(!loginI.isLoggedIn()){
			return;
		}
		Key<Match> matchKey = Key.create(Match.class, matchId);
		Match match = ofy().load().key(matchKey).now();
//		System.out.println("From match player" + match.getPlayer());
//		User user = userService.getCurrentUser();
		Key<PlayerOnline> playerKey = Key.create(PlayerOnline.class, loginI.getUserFBId());//user.getEmail()); // Check
		Key<PlayerOnline> opponentKey = match.getOpponent(playerKey); // Check 
		PlayerOnline player = ofy().load().key(playerKey).now();
		PlayerOnline opponent = ofy().load().key(opponentKey).now();
//		System.out.println("In makeMove player " + player.getUserEmail());
//		System.out.println("In makeMove opponent " + opponent.getUserEmail());
		try{
		StateChanger stateChanger = new StateChanger();
		State currentState = State.unserializeState(state);
		stateChanger.makeMove(currentState, new Move(currentState.getTurn(), State.getRankFromString(move)));
//		System.out.println("Right after makeMove");
//		String msg = "opponentmoved#"+matchId+"#"+State.serializeState(currentState)+"#"+move;
		state = State.serializeState(currentState);
		String msg = "opponentmoved#"+matchId+"#"+state+"#"+move;
//		System.out.println("Before getting tokens");
		Set<String> tokens = opponent.getTokens();
		Set<String> tokplayer = player.getTokens();
//		System.out.println("After getting tokens");
		for(String connection : tokens){
			channelService.sendMessage(new ChannelMessage(connection, msg));
//			System.out.println("Opponent's connections " + connection);
		}
		for(String connection : tokplayer){
			channelService.sendMessage(new ChannelMessage(connection, msg));
//			System.out.println("Opponent's connections " + connection);
		}
//		System.out.println("Connections must have been printed");
		match.setState(State.serializeState(currentState));
//		System.out.println("about to save match");
//		ofy().save().entity(match).now();
//		ofy().save().entities(opponent, player, match).now();
//		System.out.println("In Server MakeMove after changing state and saving match");
		// Check if game over
		if(currentState.getGameResult() != null){
			int winner = currentState.getGameResult().getWinner();
			if(winner == State.PLAYER1){
				glickoRatings(false, match, player, opponent);
			}
			else if(winner == State.PLAYER2){
				glickoRatings(false, match, opponent, player);
			}
			else{
				glickoRatings(true, match, player, opponent);
			}
			int lowR = (int)(player.getRating()-(2*player.getRatingDev()));
			int hiR = (int)(player.getRating()+(2*player.getRatingDev()));
			String msgPlayer = "glicko#[" + lowR + ", " + hiR + "]";

			int lowROpp = (int)(opponent.getRating()-(2*opponent.getRatingDev()));
			int hiROpp = (int)(opponent.getRating()+(2*opponent.getRatingDev()));
			String msgOpp = "glicko#[" + lowROpp + ", " + hiROpp + "]";
			 
			for(String connection : tokens){
				channelService.sendMessage(new ChannelMessage(connection, msgOpp));
			}
			for(String connection : tokplayer){
				channelService.sendMessage(new ChannelMessage(connection, msgPlayer));
			}
		}
		ofy().save().entities(opponent, player, match).now();
		
		} catch(RuntimeException e){
			System.out.println("Caught an exception in makeMove");
		}
	}
	
	/**
	 * Calculates the glicko rating of the player after a match has ended
	 * @param tie
	 * @param match
	 * @param player
	 * @param opponent
	 */
	private void glickoRatings(boolean tie, Match match, /*Key<PlayerOnline> playerKey, Key<PlayerOnline> opponentKey,*/ PlayerOnline player,
            PlayerOnline opponent) {
	    double s = 1;
	    if(tie){
	    	s = 0.5;
		}
	
	    Date today = new Date();
	    int t = GlickoRating.getNumDays(new Date(match.getMatchStartDate()), today);
	    double playerNewRD = GlickoRating.newRD(player.getRating(), player.getRatingDev(), opponent.getRating(), opponent.getRatingDev(), t);
	    double opponentNewRD = GlickoRating.newRD(opponent.getRating(), opponent.getRatingDev(), player.getRating(), player.getRatingDev(), t);
	    double playerNewRating = GlickoRating.newRating(player.getRating(), player.getRatingDev(), opponent.getRating(), opponent.getRatingDev(),
	    		s, t);
	    double opponentNewRating = GlickoRating.newRating(opponent.getRating(), opponent.getRatingDev(), player.getRating(), player.getRatingDev(),
	            1 - s, t);
	    player.setRating(playerNewRating);
	    player.setRatingDev(playerNewRD);
	    opponent.setRating(opponentNewRating);
	    opponent.setRatingDev(opponentNewRD);
	}
	
	/* 
	 * (non-Javadoc)
	 * @see org.munoz.client.GoFishService#matchPlayer(java.lang.String)
	 */
	@Override
	public String matchPlayer(String playerId, String state){
		ChannelService channelService = ChannelServiceFactory.getChannelService();
		// player has an opponent already. 
		if(hash.containsKey(playerId)){
			channelService.sendMessage(new ChannelMessage(waitingPlayer, "opponentsName_" + playerId));
			channelService.sendMessage(new ChannelMessage(playerId, "opponentsName_" + waitingPlayer));

			channelService.sendMessage(new ChannelMessage(waitingPlayer, state));
			channelService.sendMessage(new ChannelMessage(playerId, state));
			return PLAYERSMATCHEDALREADY;
		}
		// player is second player of a match. 
		else if(hash.values().contains(playerId)){
			channelService.sendMessage(new ChannelMessage(waitingPlayer, "opponentsName_" + playerId));
			channelService.sendMessage(new ChannelMessage(playerId, "opponentsName_" + waitingPlayer));

			channelService.sendMessage(new ChannelMessage(waitingPlayer, state));
			channelService.sendMessage(new ChannelMessage(playerId, state));
			return PLAYERSMATCHEDALREADY;
		}
		else if(waitingPlayer.compareTo("") == 0){
			waitingPlayer = playerId;
			return PLAYER1;
		}
		else {
			// register Match
			hash.put(waitingPlayer, playerId);
			// System.out.println(waitingPlayer + " matched with " + playerId);
			// System.out.println("Size of hash = " + hash.size());
			// Sends opponent's name
			channelService.sendMessage(new ChannelMessage(waitingPlayer, "opponentsName_" + playerId));
			channelService.sendMessage(new ChannelMessage(playerId, "opponentsName_" + waitingPlayer));

			channelService.sendMessage(new ChannelMessage(waitingPlayer, state));
			channelService.sendMessage(new ChannelMessage(playerId, state));

			// Get ready to receive next players;
			waitingPlayer = "";

			return PLAYER2;		
		}
	}
	
	/* (non-Javadoc)
	 * @see org.munoz.client.GoFishService#makeMoveAtServer(java.lang.String, java.lang.String)
	 */
	@Override
	public String makeMoveAtServer(String playerId, String state){
		ChannelService channelService = ChannelServiceFactory.getChannelService();
        String op="";
		if(hash.containsKey(playerId)){
                op = hash.get(playerId);
                channelService.sendMessage(new ChannelMessage(op, state));
                channelService.sendMessage(new ChannelMessage(playerId, state));
        }
        else if(hash.values().contains(playerId)){
            for (String key: hash.keySet()){
                if(hash.get(key).equals(playerId)) 
                        op = key;
            }
            channelService.sendMessage(new ChannelMessage(op, state));
            channelService.sendMessage(new ChannelMessage(playerId, state));
        }
        else{
        	matchPlayer(playerId, state);
        }
        return op;
	}
	
	/* 
	 * Test RCP connection from client to server. 
	 * return String
	 * (non-Javadoc)
	 * @see org.munoz.client.GoFishService#sayHi(java.lang.String, java.lang.String)
	 */
	@Override
	public String sayHi(String state, String playerId){
		return "Hi" + playerId;
	}

	


}